//#preprocessor

/*
 * File: PDFObject.java
 * Version: 1.9
 * Initial Creation: May 5, 2010 9:32:16 PM
 *
 * Copyright 2004 Sun Microsystems, Inc., 4150 Network Circle,
 * Santa Clara, California 95054, U.S.A. All rights reserved.
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of the GNU Lesser General Public
 * License as published by the Free Software Foundation; either
 * version 2.1 of the License, or (at your option) any later version.
 * 
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 * Lesser General Public License for more details.
 * 
 * You should have received a copy of the GNU Lesser General Public
 * License along with this library; if not, write to the Free Software
 * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 */
package com.sun.pdfview;

import java.io.IOException;
//#ifndef BlackBerrySDK4.5.0 | BlackBerrySDK4.6.0 | BlackBerrySDK4.6.1 | BlackBerrySDK4.7.0 | BlackBerrySDK4.7.1
import java.nio.ByteBuffer;
//#else
import com.sun.pdfview.helper.nio.ByteBuffer;
//#endif
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.Vector;

import net.rim.device.api.util.CloneableVector;
import net.rim.device.api.util.EmptyEnumeration;

import com.sun.pdfview.decode.PDFDecoder;
import com.sun.pdfview.decrypt.IdentityDecrypter;
import com.sun.pdfview.decrypt.PDFDecrypter;
import com.sun.pdfview.helper.PDFUtil;
import com.sun.pdfview.helper.SoftReference;

/**
 * a class encapsulating all the possibilities of content for
 * an object in a PDF file.
 * <p>
 * A PDF object can be a simple type, like a Boolean, a Number,
 * a String, or the Null value.  It can also be a NAME, which
 * looks like a string, but is a special type in PDF files, like
 * "/Name".
 * <p>
 * A PDF object can also be complex types, including Array;
 * Dictionary; Stream, which is a Dictionary plus an array of
 * bytes; or Indirect, which is a reference to some other
 * PDF object.  Indirect references will always be dereferenced
 * by the time any data is returned from one of the methods
 * in this class.
 *
 * @author Mike Wessler
 */
public class PDFObject
{
	/** an indirect reference*/
    public static final int INDIRECT = 0;      // PDFXref
    /** a Boolean */
    public static final int BOOLEAN = 1;      // Boolean
    /** a Number, represented as a double */
    public static final int NUMBER = 2;       // Double
    /** a String */
    public static final int STRING = 3;       // String
    /** a special string, seen in PDF files as /Name */
    public static final int NAME = 4;         // String
    /** an array of PDFObjects */
    public static final int ARRAY = 5;        // Array of PDFObject
    /** a Hashmap that maps String names to PDFObjects */
    public static final int DICTIONARY = 6;   // HashMap(String->PDFObject)
    /** a Stream: a Hashmap with a byte array */
    public static final int STREAM = 7;        // HashMap + byte[]
    /** the NULL object (there is only one) */
    public static final int NULL = 8;         // null
    /** a special PDF bare word, like R, obj, true, false, etc */
    public static final int KEYWORD = 9;      // String
    /**
     * When a value of {@link #getObjGen objNum} or {@link #getObjGen objGen},
     * indicates that the object is not top-level, and is embedded in another
     * object
     */
    public static final int OBJ_NUM_EMBEDDED = -2;
    
    /**
     * When a value of {@link #getObjGen objNum} or {@link #getObjGen objGen},
     * indicates that the object is not top-level, and is embedded directly
     * in the trailer.
     */
    public static final int OBJ_NUM_TRAILER = -1;

    /** the NULL PDFObject */
    //private static final long NULL_OBJ_ID = 0xD83DF8FB297756A0L;
    public static final PDFObject nullObj = new PDFObject(null, NULL, null);
    /*
    static
    {
    	if(nullObj == null)
    	{
    		nullObj = (PDFObject)com.sun.pdfview.ResourceManager.singletonStorageGet(NULL_OBJ_ID);
    		if(nullObj == null)
    		{
    			nullObj = new PDFObject(null, NULL, null);
    			com.sun.pdfview.ResourceManager.singletonStorageSet(NULL_OBJ_ID, nullObj);
    		}
    	}
    }
    */
    /** the type of this object */
    private int type;
    /** the value of this object. It can be a wide number of things, defined by type */
    private Object value;
    /** the encoded stream, if this is a STREAM object */
    private ByteBuffer stream;
    /** a cached version of the decoded stream */
    private SoftReference decodedStream;
    /** The filter limits used to generate the cached decoded stream */
    private Vector decodedStreamFilterLimits = null;
    /**
     * the PDFFile from which this object came, used for
     * dereferences
     */
    private PDFFile owner;
    /**
     * a cache of translated data.  This data can be
     * garbage collected at any time, after which it will
     * have to be rebuilt.
     */
    private SoftReference cache;

    /** @see #getObjNum() */
    private int objNum = OBJ_NUM_EMBEDDED;

    /** @see #getObjGen() */
    private int objGen = OBJ_NUM_EMBEDDED;
    
    /**
     * create a new simple PDFObject with a type and a value
     * @param owner the PDFFile in which this object resides, used
     * for dereferencing.  This may be null.
     * @param type the type of object
     * @param value the value.  For DICTIONARY, this is a HashMap.
     * for ARRAY it's an ArrayList.  For NUMBER, it's a Double.
     * for BOOLEAN, it's Boolean.TRUE or Boolean.FALSE.  For
     * everything else, it's a String.
     */
    public PDFObject(PDFFile owner, int type, Object value)
    {
        this.type = type;
        if (type == NAME)
        {
            value = ((String) value).intern();
        }
        else if (type == KEYWORD && value.equals("true"))
        {
            this.type = BOOLEAN;
            value = Boolean.TRUE;
        }
        else if (type == KEYWORD && value.equals("false"))
        {
            this.type = BOOLEAN;
            value = Boolean.FALSE;
        }
        this.value = value;
        this.owner = owner;
    }

    /**
     * create a new PDFObject that is the closest match to a
     * given Java object.  Possibilities include Double, String,
     * PDFObject[], HashMap, Boolean, or PDFParser.Tok,
     * which should be "true" or "false" to turn into a BOOLEAN.
     *
     * @param obj the sample Java object to convert to a PDFObject.
     * @throws PDFParseException if the object isn't one of the
     * above examples, and can't be turned into a PDFObject.
     */
    public PDFObject(Object obj) throws PDFParseException
    {
        this.owner = null;
        this.value = obj;
        if ((obj instanceof Double) || (obj instanceof Integer))
        {
            this.type = NUMBER;
        }
        else if (obj instanceof String)
        {
            this.type = NAME;
        }
        else if (obj instanceof PDFObject[])
        {
            this.type = ARRAY;
        }
        else if (obj instanceof Object[])
        {
            Object[] srcary = (Object[]) obj;
            PDFObject[] dstary = new PDFObject[srcary.length];
            for (int i = 0; i < srcary.length; i++)
            {
                dstary[i] = new PDFObject(srcary[i]);
            }
            value = dstary;
            this.type = ARRAY;
        }
        else if (obj instanceof Hashtable)
        {
            this.type = DICTIONARY;
        }
        else if (obj instanceof Boolean)
        {
            this.type = BOOLEAN;
        }
        else if (obj instanceof PDFParser.Tok)
        {
            PDFParser.Tok tok = (PDFParser.Tok)obj;
            if (tok.name.equals("true"))
            {
                this.value = Boolean.TRUE;
                this.type = BOOLEAN;
            }
            else if (tok.name.equals("false"))
            {
                this.value = Boolean.FALSE;
                this.type = BOOLEAN;
            }
            else
            {
                this.value = tok.name;
                this.type = NAME;
            }
        }
        else
        {
            throw new PDFParseException(com.sun.pdfview.ResourceManager.getResource(com.sun.pdfview.ResourceManager.LOCALIZATION).getFormattedString(com.sun.pdfview.i18n.ResourcesResource.OBJECT_BAD_RAW_TYPE, new Object[]{obj.toString()}));
        }
    }

    /**
     * create a new PDFObject based on a PDFXref
     * @param owner the PDFFile from which the PDFXref was drawn
     * @param xref the PDFXref to turn into a PDFObject
     */
    public PDFObject(PDFFile owner, PDFXref xref)
    {
        this.type = INDIRECT;
        this.value = xref;
        this.owner = owner;
    }
    
    /**
     * get the type of this object.  The object will be
     * dereferenced, so INDIRECT will never be returned.
     * @return the type of the object
     */
    public int getType() throws IOException
    {
        if (type == INDIRECT)
        {
            return dereference().getType();
        }

        return type;
    }
    
    /**
     * set the stream of this object.  It should have been
     * a DICTIONARY before the call.
     * @param data the data, as a ByteBuffer.
     */
    public void setStream(ByteBuffer data)
    {
        this.type = STREAM;
        this.stream = data;
    }

    /**
     * get the value in the cache.  May become null at any time.
     * @return the cached value, or null if the value has been
     * garbage collected.
     */
    public Object getCache() throws IOException
    {
        if (type == INDIRECT)
        {
            return dereference().getCache();
        }
        else if (cache != null)
        {
            return cache.get();
        }
        else
        {
            return null;
        }
    }
    
    /**
     * set the cached value.  The object may be garbage collected
     * if no other reference exists to it.
     * @param obj the object to be cached
     */
    public void setCache(Object obj) throws IOException
    {
        if (type == INDIRECT)
        {
            dereference().setCache(obj);
            return;
        }
        else
        {
            cache = new SoftReference(obj);
        }
    }
    
    public byte[] getStream(Vector filterLimits) throws IOException
    {
        if (type == INDIRECT)
        {
            return dereference().getStream(filterLimits);
        }
        else if (type == STREAM && stream != null)
        {
            byte[] data = null;
            
            synchronized(stream)
            {
                // decode
                ByteBuffer streamBuf = decodeStream(filterLimits);
                // ByteBuffer streamBuf = stream;
                
                // First try to use the array with no copying.  This can only
                // be done if the buffer has a backing array, and is not a slice
                if (streamBuf.hasArray() && streamBuf.arrayOffset() == 0)
                {
                    byte[] ary = streamBuf.array();
                    
                    // make sure there is no extra data in the buffer
                    if (ary.length == streamBuf.remaining())
                    {
                        return ary;
                    }
                }
                
                // Can't use the direct buffer, so copy the data (bad)
                data = new byte[streamBuf.remaining()];
                streamBuf.get(data);
                
                // return the stream to its starting position
                streamBuf.flip();
            }
            
            return data;
        }
        else if (type == STRING)
        {
            return PDFStringUtil.asBytes(getStringValue());
        }
        else
        {
	        // wrong type
	        return null;
        }
    }
    
    /**
     * get the stream from this object.  Will return null if this
     * object isn't a STREAM.
     * @return the stream, or null, if this isn't a STREAM.
     */
    public byte[] getStream() throws IOException
    {
       return getStream(new Vector());
    }
    
    /**
     * get the stream from this object as a byte buffer.  Will return null if 
     * this object isn't a STREAM.
     * @return the buffer, or null, if this isn't a STREAM.
     */
    public ByteBuffer getStreamBuffer() throws IOException
    {
        return getStreamBuffer(new Vector());
    }
    
    /**
     * get the stream from this object as a byte buffer.  Will return null if 
     * this object isn't a STREAM.
     * @return the buffer, or null, if this isn't a STREAM.
     */
    public ByteBuffer getStreamBuffer(Vector filterLimits) throws IOException
    {
        if (type == INDIRECT)
        {
            return dereference().getStreamBuffer(filterLimits);
        }
        else if (type == STREAM && stream != null)
        {
            synchronized(stream)
            {
                ByteBuffer streamBuf = decodeStream(filterLimits);
                // ByteBuffer streamBuf = stream;
                ByteBuffer dupBuf; //Closest on BlackBerry to ByteBuffer.duplicate()
                if(streamBuf.hasArray())
                {
                	//Simply get the data and wrap it
                	dupBuf = ByteBuffer.wrap(streamBuf.array());
                }
                else
                {
                	//Need to read the data into a new buffer
                	byte[] buffer = new byte[4096];
                	int pos = streamBuf.position();
                	dupBuf = ByteBuffer.allocateDirect(streamBuf.remaining());
                	while(streamBuf.remaining() > 0)
                	{
                		int rem = streamBuf.remaining();
                		if(rem < 4096)
                		{
                			buffer = new byte[rem];
                		}
                		streamBuf.get(buffer);
                		dupBuf.put(buffer);
                	}
                	streamBuf.position(pos);
                	dupBuf.flip();
                }
                return (ByteBuffer)dupBuf.position(streamBuf.position()).limit(streamBuf.limit());
                
                //From: http://www.docjar.org/html/api/java/nio/ByteBuffer.java.html
                //return new ByteBufferImpl (backing_buffer, array_offset, capacity(), limit(), position(), mark, isReadOnly()); //Buffer.duplicate ()
                //return new ByteBufferImpl (array, 0, array.length, offset + length, offset, -1, false); //Buffer.wrap(Buffer)
            }
        }
        else if (type == STRING)
        {
            String src = getStringValue();
            return ByteBuffer.wrap(src.getBytes());
        }
        
        // wrong type
        return null;
    }

    /**
     * Get the decoded stream value
     */
    private ByteBuffer decodeStream(Vector filterLimits) throws IOException
    {
        ByteBuffer outStream = null;
        
        // first try the cache
        if (decodedStream != null && PDFUtil.Vector_equals(filterLimits, decodedStreamFilterLimits))
        {
            outStream = (ByteBuffer)decodedStream.get();
        }
        
        // no luck in the cache, do the actual decoding
        if (outStream == null)
        {
            stream.rewind();
            outStream = PDFDecoder.decodeStream(this, stream, filterLimits);
            decodedStreamFilterLimits = CloneableVector.clone(filterLimits);
            decodedStream = new SoftReference(outStream);
        }
        
        return outStream;
    }
    
    /**
     * get the value as an int.  Will return 0 if this object
     * isn't a NUMBER.
     */
    public int getIntValue() throws IOException
    {
        if (type == INDIRECT)
        {
            return dereference().getIntValue();
        }
        else if (type == NUMBER)
        {
            return ((Double)value).intValue();
        }
        
        // wrong type
        return 0;
    }

    /**
     * get the value as a float.  Will return 0 if this object
     * isn't a NUMBER
     */
    public float getFloatValue() throws IOException
    {
        if (type == INDIRECT)
        {
            return dereference().getFloatValue();
        }
        else if (type == NUMBER)
        {
            return ((Double)value).floatValue();
        }
        
        // wrong type
        return 0;
    }

    /**
     * get the value as a double.  Will return 0 if this object
     * isn't a NUMBER.
     */
    public double getDoubleValue() throws IOException
    {
        if (type == INDIRECT)
        {
            return dereference().getDoubleValue();
        }
        else if (type == NUMBER)
        {
            return ((Double)value).doubleValue();
        }
        
        // wrong type
        return 0;
    }

    /**
     * get the value as a String.  Will return null if the object
     * isn't a STRING, NAME, or KEYWORD.  This method will <b>NOT</b>
     * convert a NUMBER to a String. If the string is actually
     * a text string (i.e., may be encoded in UTF16-BE or PdfDocEncoding),
     * then one should use {@link #getTextStringValue()} or use one
     * of the {@link PDFStringUtil} methods on the result from this
     * method. The string value represents exactly the sequence of 8 bit
     * characters present in the file, decrypted and decoded as appropriate,
     * into a string containing only 8 bit character values - that is, each
     * char will be between 0 and 255.
     */
    public String getStringValue() throws IOException
    {
        if (type == INDIRECT)
        {
            return dereference().getStringValue();
        }
        else if (type == STRING || type == NAME || type == KEYWORD)
        {
            return (String)value;
        }
        
        // wrong type
        return null;
    }
    
    /**
     * Get the value as a text string; i.e., a string encoded in UTF-16BE
     * or PDFDocEncoding. Simple latin alpha-numeric characters are preserved in
     * both these encodings.
     * @return the text string value
     * @throws IOException
     */
    public String getTextStringValue() throws IOException
    {
    	return PDFStringUtil.asTextString(getStringValue());
    }

    /**
     * get the value as a PDFObject[].  If this object is an ARRAY,
     * will return the array.  Otherwise, will return an array
     * of one element with this object as the element.
     */
    public PDFObject[] getArray() throws IOException
    {
        if (type == INDIRECT)
        {
            return dereference().getArray();
        }
        else if (type == ARRAY)
        {
            PDFObject[] ary = (PDFObject[])value;
            return ary;
        }
        else
        {
            PDFObject[] ary = new PDFObject[1];
            ary[0] = this;
            return ary;
        }
    }

    /**
     * get the value as a boolean.  Will return false if this
     * object is not a BOOLEAN
     */
    public boolean getBooleanValue() throws IOException
    {
        if (type == INDIRECT)
        {
            return dereference().getBooleanValue();
        }
        else if (type == BOOLEAN)
        {
            return value == Boolean.TRUE;
        }
        
        // wrong type
        return false;
    }
    
    /**
     * if this object is an ARRAY, get the PDFObject at some
     * position in the array.  If this is not an ARRAY, returns
     * null.
     */
    public PDFObject getAt(int idx) throws IOException
    {
        if (type == INDIRECT)
        {
            return dereference().getAt(idx);
        }
        else if (type == ARRAY)
        {
            PDFObject[] ary = (PDFObject[])value;
            return ary[idx];
        }
        
        // wrong type
        return null;
    }

    /**
     * get an Iterator over all the keys in the dictionary.  If
     * this object is not a DICTIONARY or a STREAM, returns an
     * Iterator over the empty list.
     */
    public Enumeration getDictKeys() throws IOException
    {
        if (type == INDIRECT)
        {
            return dereference().getDictKeys();
        }
        else if (type == DICTIONARY || type == STREAM)
        {
            return ((Hashtable)value).keys();
        }
        
        // wrong type
        return new EmptyEnumeration();
    }
    
    /**
     * get the dictionary as a Hashtable.  If this isn't a DICTIONARY
     * or a STREAM, returns null
     */
    public Hashtable getDictionary() throws IOException
    {
        if (type == INDIRECT)
        {
            return dereference().getDictionary();
        }
        else if (type == DICTIONARY || type == STREAM)
        {
            return (Hashtable)value;
        }
        
        // wrong type
        return new Hashtable();
    }

    /**
     * get the value associated with a particular key in the
     * dictionary.  If this isn't a DICTIONARY or a STREAM,
     * or there is no such key, returns null.
     */
    public PDFObject getDictRef(String key) throws IOException
    {
        if (type == INDIRECT)
        {
            return dereference().getDictRef(key);
        }
        else if (type == DICTIONARY || type == STREAM)
        {
            key = key.intern();
            Hashtable h = (Hashtable)value;
            PDFObject obj = (PDFObject)h.get(key.intern());
            return obj;
        }
        
        // wrong type
        return null;
    }
    
    /**
     * returns true only if this object is a DICTIONARY or a
     * STREAM, and the "Type" entry in the dictionary matches a
     * given value.
     * @param match the expected value for the "Type" key in the
     * dictionary
     * @return whether the dictionary is of the expected type
     */
    public boolean isDictType(String match) throws IOException
    {
        if (type == INDIRECT)
        {
            return dereference().isDictType(match);
        }
        else if (type != DICTIONARY && type != STREAM)
        {
            return false;
        }
        
        PDFObject obj = getDictRef("Type");
        return obj != null && obj.getStringValue().equals(match);
    }

    public PDFDecrypter getDecrypter()
    {
    	// PDFObjects without owners are always created as part of
        // content instructions. Such an object will never have encryption
        // applied to it, as the stream that contains it is the
        // unit of encryption, with no further encryption being applied
        // within. So if someone asks for the decrypter for
        // one of these in-stream objects, no decryption should
        // ever be applied. This can be seen with inline images.
        return owner != null ? owner.getDefaultDecrypter() : IdentityDecrypter.getInstance();
    }

     /**
     * Set the object identifiers
     * @param objNum the object number
     * @param objGen the object generation number
     */
    public void setObjectId(int objNum, int objGen)
    {
    	PDFUtil.assert(objNum >= OBJ_NUM_TRAILER, "objNum >= OBJ_NUM_TRAILER");
    	PDFUtil.assert(objGen >= OBJ_NUM_TRAILER, "objGen >= OBJ_NUM_TRAILER");
        this.objNum = objNum;
        this.objGen = objGen;
    }
    
    /**
     * Get the object number of this object; a negative value indicates that
     * the object is not numbered, as it's not a top-level object: if the value
     * is {@link #OBJ_NUM_EMBEDDED}, it is because it's embedded within
     * another object. If the value is {@link #OBJ_NUM_TRAILER}, it's because
     * it's an object from the trailer.
     * @return the object number, if positive
     */
    public int getObjNum()
    {
        return objNum;
    }

    /**
     * Get the object generation number of this object; a negative value
     * indicates that the object is not numbered, as it's not a top-level
     * object: if the value is {@link #OBJ_NUM_EMBEDDED}, it is because it's
     * embedded within another object. If the value is {@link
     * #OBJ_NUM_TRAILER}, it's because it's an object from the trailer.
     * @return the object generation number, if positive
     */
    public int getObjGen()
    {
        return objGen;
    }

    /**
     * return a representation of this PDFObject as a String.
     * Does NOT dereference anything:  this is the only method
     * that allows you to distinguish an INDIRECT PDFObject.
     */
    public String toString()
    {
        try
        {
            if (type == INDIRECT)
            {
                StringBuffer str = new StringBuffer();
                str.append("Indirect to #" + ((PDFXref) value).getObjectNumber());
                try
                {
                    str.append("\n" + dereference().toString());
                }
                catch (Throwable t)
                {
                    str.append(t.toString());
                }
                return str.toString();
            }
            else if (type == BOOLEAN)
            {
                return "Boolean: " + (getBooleanValue() ? "true" : "false");
            }
            else if (type == NUMBER)
            {
                return "Number: " + getDoubleValue();
            }
            else if (type == STRING)
            {
                return "String: " + getStringValue();
            }
            else if (type == NAME)
            {
                return "Name: /" + getStringValue();
            }
            else if (type == ARRAY)
            {
                return "Array, length=" + ((PDFObject[])value).length;
            }
            else if (type == DICTIONARY)
            {
                StringBuffer sb = new StringBuffer();
                PDFObject obj = getDictRef("Type");
                if (obj != null)
                {
                    sb.append(obj.getStringValue());
                    obj = getDictRef("Subtype");
                    if (obj == null)
                    {
                        obj = getDictRef("S");
                    }
                    if (obj != null)
                    {
                        sb.append("/" + obj.getStringValue());
                    }
                }
                else
                {
                    sb.append("Untyped");
                }
                sb.append(" dictionary. Keys:");
                Hashtable hm = (Hashtable)value;
                Enumeration keyEn = hm.keys();
                Enumeration valueEn = hm.elements();
                while (keyEn.hasMoreElements()) //There should be the same number of keys as elements
                {
                    sb.append("\n   " + keyEn.nextElement() + "  " + valueEn.nextElement());
                }
                return sb.toString();
            }
            else if (type == STREAM)
            {
                byte[] st = getStream();
                if (st == null)
                {
                    return "Broken stream";
                }
                return "Stream: [[" + new String(st, 0, st.length > 30 ? 30 : st.length) + "]]";
            }
            else if (type == NULL)
            {
                return "Null";
            }
            else if (type == KEYWORD)
            {
                return "Keyword: " + getStringValue();
            /*
            }
            else if (type == IMAGE)
            {
	            StringBuffer sb = new StringBuffer();
	            java.awt.Image im = (java.awt.Image)stream;
	            sb.append("Image ("+im.getWidth(null)+"x"+im.getHeight(null)+", with keys:");
	            HashMap hm = (HashMap)value;
	            Iterator it = hm.keySet().iterator();
	            while(it.hasNext())
	            {
	            	sb.append(" "+(String)it.next());
	            }
	            return sb.toString();
            */
            }
            else
            {
                return "Whoops!  big error!  Unknown type";
            }
        }
        catch (IOException ioe)
        {
            return "Caught an error: " + ioe;
        }
    }
    
    /**
     * Make sure that this object is dereferenced.  Use the cache of
     * an indirect object to cache the dereferenced value, if possible.
     */
    public PDFObject dereference() throws IOException
    {
        if (type == INDIRECT)
        {
            PDFObject obj = null;
            
            if (cache != null)
            {
                obj = (PDFObject)cache.get();
            }
            
            if (obj == null || obj.value == null)
            {
                if (owner == null)
                {
                    System.out.println("Bad seed (owner == null)!  Object=" + this);
                }
                
                obj = owner.dereference((PDFXref)value, getDecrypter());
                
                cache = new SoftReference(obj);
            }
            
            return obj;
        }
        else
        {
            // not indirect, no need to dereference
            return this;
        }
    }

    /**
     * Identify whether the object is currently an indirect/cross-reference
     * @return whether currently indirect
     */
    public boolean isIndirect()
    {
        return (type == INDIRECT);
    }

    /** 
     * Test whether two PDFObject are equal.  Objects are equal IFF they
     * are the same reference OR they are both indirect objects with the
     * same id and generation number in their xref
     */
    public boolean equals(Object o)
    {
        if (super.equals(o))
        {
            // they are the same object
            return true;
        }
        else if (type == INDIRECT && o instanceof PDFObject)
        {
            // they are both PDFObjects.  Check type and xref.
            PDFObject obj = (PDFObject) o;
            
            if (obj.type == INDIRECT)
            {
                PDFXref lXref = (PDFXref)value;
                PDFXref rXref = (PDFXref)obj.value;

                return ((lXref.getObjectNumber() == rXref.getObjectNumber()) && (lXref.getGeneration() == rXref.getGeneration()));
            }
        }
        
        return false;
    }
}
